/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package serialN64Controller;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.util.EnumMap;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;

/**
 *
 * @author marcos
 */
public class N64Controller {

    static public enum Button {

        A, B, Z, START,
        D_UP, D_DOWN, D_LEFT, D_RIGHT,
        _UNUSED1, _UNUSED2, // only to easy parse
        L, R,
        C_UP, C_DOWN, C_LEFT, C_RIGHT,
        A_UP, A_DOWN, A_LEFT, A_RIGHT; // for use when mapping analog to keys   

        public boolean isValid() {
            return !this.equals(_UNUSED1) && !this.equals(_UNUSED2);
        }
    }

    static public enum Analog {

        MAP_KEYS, // also incremental
        INCREMENTAL_MOUSE,
        ABSOLUTE_MOUSE
    }
    public static final EnumMap<Button, Integer> defaultButtonsMap;

    static {
        defaultButtonsMap = new EnumMap<>(Button.class);
        defaultButtonsMap.put(Button.A, KeyEvent.VK_A);
        defaultButtonsMap.put(Button.B, KeyEvent.VK_B);
        defaultButtonsMap.put(Button.Z, KeyEvent.VK_Z);
        defaultButtonsMap.put(Button.START, KeyEvent.VK_S);
        defaultButtonsMap.put(Button.D_UP, KeyEvent.VK_U);
        defaultButtonsMap.put(Button.D_DOWN, KeyEvent.VK_J);
        defaultButtonsMap.put(Button.D_LEFT, KeyEvent.VK_H);
        defaultButtonsMap.put(Button.D_RIGHT, KeyEvent.VK_K);
        defaultButtonsMap.put(Button.L, KeyEvent.VK_L);
        defaultButtonsMap.put(Button.R, KeyEvent.VK_R);
        defaultButtonsMap.put(Button.C_UP, KeyEvent.VK_1);
        defaultButtonsMap.put(Button.C_DOWN, KeyEvent.VK_4);
        defaultButtonsMap.put(Button.C_LEFT, KeyEvent.VK_2);
        defaultButtonsMap.put(Button.C_RIGHT, KeyEvent.VK_3);
        defaultButtonsMap.put(Button.A_UP, KeyEvent.VK_UP);
        defaultButtonsMap.put(Button.A_DOWN, KeyEvent.VK_DOWN);
        defaultButtonsMap.put(Button.A_LEFT, KeyEvent.VK_LEFT);
        defaultButtonsMap.put(Button.A_RIGHT, KeyEvent.VK_RIGHT);
    }
    protected static Dimension defaultThreshold = new Dimension(5, 5);
    public static int toogleCountMax = 3;

    public static Point mouseActualPos() {
        return MouseInfo.getPointerInfo().getLocation();
    }
    private Robot robot;
    private EnumMap<Button, Integer> buttonMap;
    private EnumMap<N64Controller.Button, Boolean> pressedButtons = new EnumMap<>(N64Controller.Button.class);
    private Dimension thresholdMoveKeys;
    private Analog analog;
    // toogle mouse button:
    private boolean mouseOn = true;
    private Button toogleButton = Button.L;
    private int toogleCount;
    // calibration:
    private Point analogMax = new Point(Byte.MAX_VALUE, Byte.MAX_VALUE);
    private Point analogMin = new Point(Byte.MIN_VALUE, Byte.MIN_VALUE);
    private Dimension analogDeadMax = new Dimension(0, 0);
    private Dimension analogDeadMin = new Dimension(0, 0);
    private double incrementRatio = 1;
    private Rectangle screenBounds;
    // turbo:
    private int turboDelay = -1;
    private Timer relaeaseKeyTimer = new Timer(true);

    static protected class RelaeaseKeyTimerTask extends TimerTask {

        N64Controller n64Controller;
        Button button;

        public RelaeaseKeyTimerTask(N64Controller n64Controller, Button button) {
            this.n64Controller = n64Controller;
            this.button = button;
        }

        @Override
        public void run() {
            n64Controller.release(button);
        }
    }

    public N64Controller(EnumMap<Button, Integer> buttonMap, Analog kind, Dimension thresholdMoveKeys) throws AWTException {
        this.buttonMap = new EnumMap<>(buttonMap);
        robot = new Robot();
        screenBounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getBounds();

        analog = kind;
        this.thresholdMoveKeys = thresholdMoveKeys;

        for (Button b : N64Controller.Button.values()) {
            pressedButtons.put(b, false);
        }
    }

    public N64Controller(EnumMap<Button, Integer> buttonMap, Dimension threshold) throws AWTException {
        this(buttonMap, Analog.MAP_KEYS, threshold);
    }

    public N64Controller(EnumMap<Button, Integer> buttonMap) throws AWTException {
        this(buttonMap, Analog.MAP_KEYS, defaultThreshold);
    }

    public N64Controller(Analog kind) throws AWTException {
        this(defaultButtonsMap, kind, defaultThreshold);
    }

    public N64Controller() throws AWTException {
        this(defaultButtonsMap, Analog.MAP_KEYS, defaultThreshold);
    }

    public N64Controller(N64Controller other) throws AWTException {
        this(other.buttonMap, other.analog, other.thresholdMoveKeys);
    }

    public void tap(Button b) {
        press(b);
        release(b);
    }

    final public void press(Button b) {
        if (!b.isValid()) {
            return;
        }
        if (tryToogleMouseOn(b)) {
            return;
        }
        if (pressedButtons.get(b)) {
            return;
        }

        doPress(b);
        pressedButtons.put(b, true);
        if (turboDelay == 0) {
            release(b);
        } else if (turboDelay > 0) {
            relaeaseKeyTimer.schedule(new RelaeaseKeyTimerTask(this, b), turboDelay);
        }
    }

    synchronized protected void doPress(Button b) {
        robot.keyPress(buttonMap.get(b));
    }

    final public void release(Button b) {
        if (!b.isValid()) {
            return;
        }
        if (!pressedButtons.get(b)) {
            return;
        }
        doRelease(b);
        pressedButtons.put(b, false);
    }

    synchronized protected void doRelease(Button b) {
        robot.keyRelease(buttonMap.get(b));
    }

    public void releaseAll() {
        for (Button b : buttonMap.keySet()) {
            release(b);
        }
    }

    public void move(Point p) {
        if (!isMouseOn()) {
            moveKeys(p);
            return;
        }
        switch (analog) {
            case MAP_KEYS:
                moveKeys(p);
                break;
            case INCREMENTAL_MOUSE:
                moveIncrementalMouse(p);
                break;
            case ABSOLUTE_MOUSE:
                moveAbsoluteMouse(p);
                break;
        }
    }

    private void moveKeys(Point p) {
        if (p.y > thresholdMoveKeys.height) {
            press(Button.A_UP);
            release(Button.A_DOWN);
        } else if (p.y < -thresholdMoveKeys.height) {
            press(Button.A_DOWN);
            release(Button.A_UP);
        } else {
            release(Button.A_UP);
            release(Button.A_DOWN);
        }

        if (p.x > thresholdMoveKeys.width) {
            press(Button.A_RIGHT);
            release(Button.A_LEFT);
        } else if (p.x < -thresholdMoveKeys.width) {
            press(Button.A_LEFT);
            release(Button.A_RIGHT);
        } else {
            release(Button.A_LEFT);
            release(Button.A_RIGHT);
        }
    }

    protected Point applyDeadZone(Point p) {
        p = new Point(p);
        if (p.y < analogDeadMax.height && p.y > analogDeadMin.height) {
            p.y = 0;
        }
        if (p.x < analogDeadMax.width && p.x > analogDeadMin.width) {
            p.x = 0;
        }
        return p;
    }

    private void moveIncrementalMouse(Point p) {
        p = applyDeadZone(p);
        if (p.x == 0 && p.y == 0) {
            return;
        }
        Point mousePos = mouseActualPos();

        mousePos.x += incrementRatio * p.x;
        mousePos.y -= incrementRatio * p.y;

        robot.mouseMove(mousePos.x, mousePos.y);
    }

    private void moveAbsoluteMouse(Point p) {
        p = applyDeadZone(p);
        if (p.x == 0 && p.y == 0) {
            return;
        }
        double ax = ((double) screenBounds.width - screenBounds.x) / (analogMax.x - analogMin.x);
        double bx = ((double) screenBounds.x) - ax * analogMin.x;
        double ay = ((double) screenBounds.height - screenBounds.y) / (analogMax.y - analogMin.y);
        double by = ((double) screenBounds.y) - ay * analogMin.y;

        int x = (int) (ax * p.x + bx);
        int y = (int) (ay * p.y + by);
        robot.mouseMove(x, screenBounds.height - y + screenBounds.y);
    }

    public boolean isMouseOn() {
        return mouseOn;
    }

    public void setMouseOn(boolean b) {
        mouseOn = b;
    }

    private boolean tryToogleMouseOn(Button b) {
        if (analog == Analog.MAP_KEYS || toogleButton == null || b != toogleButton) {
            toogleCount = 0;
            return false;
        }
        System.out.println("toogle " + toogleCount);
        if (++toogleCount >= toogleCountMax) {
            toogleMouseOn();
            toogleCount = 0;
            return true;
        }
        return false;
    }

    public void toogleMouseOn() {
        mouseOn = !mouseOn;
    }

    public Button getToogleButton() {
        return toogleButton;
    }

    public void setToogleButton(Button toogleButton) {
        this.toogleButton = toogleButton;
    }

    public void disableToogleButton() {
        this.toogleButton = null;
    }

    public void calibrate(Point analogMax, Point analogMin, Dimension analogDeadMax, Dimension analogDeadMin) {
        screenBounds = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().getBounds();
        this.analogMax = analogMax;
        this.analogMin = analogMin;
        this.analogDeadMax = analogDeadMax;
        this.analogDeadMin = analogDeadMin;
    }

    public Analog getAnalogKind() {
        return analog;
    }

    public void setAnalogKind(Analog analog) {
        this.analog = analog;
    }

    public double getIncrementRatio() {
        return incrementRatio;
    }

    public void setIncrementRatio(double incrementRatio) {
        this.incrementRatio = incrementRatio;
    }

    public Dimension getAnalogDeadMax() {
        return analogDeadMax;
    }

    public Dimension getAnalogDeadMin() {
        return analogDeadMin;
    }

    public Point getAnalogMax() {
        return analogMax;
    }

    public Point getAnalogMin() {
        return analogMin;
    }

    public EnumMap<Button, Boolean> getPressedButtons() {
        return pressedButtons;
    }

    public void enableTurbo(int turboDelay) {
        this.turboDelay = turboDelay;
    }

    public void disableTurbo() {
        this.turboDelay = -1;
    }

    public int getTurboDelay() {
        return turboDelay;
    }

    public int getKey(Button b) {
        return buttonMap.get(b).intValue();
    }

    public int getKey(String b) {
        return buttonMap.get(Button.valueOf(b)).intValue();
    }

    public void setKey(Button b, int keyCode) {
        buttonMap.put(b, keyCode);
    }

    public void setKey(String b, int keyCode) {
        buttonMap.put(Button.valueOf(b), keyCode);
    }

    public EnumMap<Button, Integer> getButtonMap() {
        return new EnumMap<>(buttonMap);
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 47 * hash + Objects.hashCode(this.buttonMap);
        hash = 47 * hash + Objects.hashCode(this.thresholdMoveKeys);
        hash = 47 * hash + (this.analog != null ? this.analog.hashCode() : 0);
        hash = 47 * hash + (this.mouseOn ? 1 : 0);
        hash = 47 * hash + (this.toogleButton != null ? this.toogleButton.hashCode() : 0);
        hash = 47 * hash + this.toogleCount;
        hash = 47 * hash + Objects.hashCode(this.analogMax);
        hash = 47 * hash + Objects.hashCode(this.analogMin);
        hash = 47 * hash + Objects.hashCode(this.analogDeadMax);
        hash = 47 * hash + Objects.hashCode(this.analogDeadMin);
        hash = 47 * hash + (int) (Double.doubleToLongBits(this.incrementRatio) ^ (Double.doubleToLongBits(this.incrementRatio) >>> 32));
        hash = 47 * hash + Objects.hashCode(this.screenBounds);
        hash = 47 * hash + this.turboDelay;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final N64Controller other = (N64Controller) obj;
        if (!this.buttonMap.equals(other.buttonMap)) {
            return false;
        }
        if (!this.thresholdMoveKeys.equals(other.thresholdMoveKeys)) {
            return false;
        }
        if (this.analog != other.analog) {
            return false;
        }
        if (this.mouseOn != other.mouseOn) {
            return false;
        }
        if (this.toogleButton != other.toogleButton) {
            return false;
        }
        if (this.toogleCount != other.toogleCount) {
            return false;
        }
        if (!this.analogMax.equals(other.analogMax)) {
            return false;
        }
        if (!this.analogMin.equals(other.analogMin)) {
            return false;
        }
        if (!this.analogDeadMax.equals(other.analogDeadMax)) {
            return false;
        }
        if (!this.analogDeadMin.equals(other.analogDeadMin)) {
            return false;
        }
        if (Double.doubleToLongBits(this.incrementRatio) != Double.doubleToLongBits(other.incrementRatio)) {
            return false;
        }
        if (!this.screenBounds.equals(other.screenBounds)) {
            return false;
        }
        if (this.turboDelay != other.turboDelay) {
            return false;
        }
        return true;
    }
}
